package com.abso.mp3tunes.locker.ui.viewers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;

import com.abso.mp3tunes.locker.core.LockerServiceException;
import com.abso.mp3tunes.locker.core.data.Album;
import com.abso.mp3tunes.locker.core.data.Artist;
import com.abso.mp3tunes.locker.core.data.Playlist;
import com.abso.mp3tunes.locker.core.data.Track;
import com.abso.mp3tunes.locker.ui.LockerPlugin;
import com.abso.mp3tunes.locker.ui.jobs.GetTreeChildNodesJob;

/**
 * The tree content provider for Locker tree viewers.
 */
public class LockerViewerContentProvider implements ITreeContentProvider {

	/** The tree viewer. */
	private TreeViewer viewer;

	/** A map of artist/album pairs. */
	private Map albumsByArtists = new HashMap();

	/** A map of album/track pairs. */
	private Map tracksByAlbums = new HashMap();

	/** A map of playlist/track pairs. */
	private Map tracksByPlaylist = new HashMap();

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
	 */
	public Object[] getChildren(Object parentElement) {
		if (parentElement instanceof Object[]) {
			return (Object[]) parentElement;
		} else if (parentElement instanceof Artist) {
			final Artist artist = (Artist) parentElement;
			if (albumsByArtists.get(artist) == null) {
				createGetArtistChildrenJob(artist).schedule();
			}
			return (Object[]) albumsByArtists.get(artist);
		} else if (parentElement instanceof Album) {
			final Album album = (Album) parentElement;
			if (tracksByAlbums.get(album) == null) {
				createGetAlbumChildrenJob(album).schedule();
			}
			return (Object[]) tracksByAlbums.get(album);
		} else if (parentElement instanceof Playlist) {
			final Playlist playlist = (Playlist) parentElement;
			if (tracksByPlaylist.get(playlist) == null) {
				createGetPlaylistChildrenJob(playlist).schedule();
			}
			return (Object[]) tracksByPlaylist.get(playlist);
		} else {
			return new Object[0];
		}
	}

	/**
	 * Clears cached children, thus forcing the content provider to recompute
	 * the children when required.
	 */
	public void clearAllCachedChildren() {
		albumsByArtists.clear();
		tracksByAlbums.clear();
		tracksByPlaylist.clear();
	}

	/**
	 * Removes the cached children for the specified node.
	 * 
	 * @param item
	 *            the node whose cached children must be removed.
	 */
	public void removeCachedChildren(Object item) {
		albumsByArtists.remove(item);
		tracksByAlbums.remove(item);
		tracksByPlaylist.remove(item);
	}

	/**
	 * Creates a background job able to return the child nodes of a specific
	 * playlist.
	 * 
	 * @param playlist
	 *            the playlist whose child nodes must be returned.
	 * @return the background job.
	 */
	private GetTreeChildNodesJob createGetPlaylistChildrenJob(
			final Playlist playlist) {
		return new GetTreeChildNodesJob("Refresh tracks of "
				+ playlist.getTitle(), playlist, tracksByPlaylist, viewer) {
			protected Object[] doGetChildren() throws LockerServiceException {
				return LockerPlugin.getDefault().getLockerService()
						.getTracksByPlaylist(playlist.getId());
			}

		};
	}

	/**
	 * Creates a background job able to return the child nodes of a specific
	 * album.
	 * 
	 * @param album
	 *            the album whose child nodes must be returned.
	 * @return the background job.
	 */
	private GetTreeChildNodesJob createGetAlbumChildrenJob(final Album album) {
		return new GetTreeChildNodesJob(
				"Refresh tracks of " + album.getTitle(), album, tracksByAlbums,
				viewer) {
			protected Object[] doGetChildren() throws LockerServiceException {
				return LockerPlugin.getDefault().getLockerService()
						.getTracksByAlbum(album.getId());
			}

		};
	}

	/**
	 * Creates a background job able to return the child nodes of a specific
	 * artist.
	 * 
	 * @param artist
	 *            the artist whose child nodes must be returned.
	 * @return the background job.
	 */
	private GetTreeChildNodesJob createGetArtistChildrenJob(final Artist artist) {
		return new GetTreeChildNodesJob(
				"Refresh albums of " + artist.getName(), artist,
				albumsByArtists, viewer) {
			protected Object[] doGetChildren() throws LockerServiceException {
				return LockerPlugin.getDefault().getLockerService()
						.getAlbumsByArtist(artist.getId());
			}
		};
	}

	/**
	 * Returns the child nodes of a specific element, waiting for the background
	 * job to complete.
	 * 
	 * @param parentElement
	 *            the parent element.
	 * @param monitor
	 *            the progress monitor.
	 * @return the child nodes.
	 */
	public Object[] getChildrenSync(Object parentElement,
			IProgressMonitor monitor) {
		if (monitor.isCanceled()) {
			return new Object[0];
		}
		if (parentElement instanceof Album) {
			if (isPendingOrNullChildren(parentElement, tracksByAlbums)) {
				Job job = createGetAlbumChildrenJob((Album) parentElement);
				job.schedule();
				try {
					job.join();
					return getChildren(parentElement);
				} catch (InterruptedException e) {
				}
			} else {
				return (Object[]) tracksByAlbums.get(parentElement);
			}
		} else if (parentElement instanceof Artist) {
			Object[] albums = new Object[0];
			if (isPendingOrNullChildren(parentElement, albumsByArtists)) {
				Job job = createGetArtistChildrenJob((Artist) parentElement);
				job.schedule();
				try {
					job.join();
					albums = getChildren(parentElement);
				} catch (InterruptedException e) {
				}
			} else {
				albums = (Object[]) albumsByArtists.get(parentElement);
			}
			List tracks = new ArrayList();
			for (int i = 0; i < albums.length; i++) {
				tracks.addAll(Arrays
						.asList(getChildrenSync(albums[i], monitor)));
			}
			return tracks.toArray();
		} else if (parentElement instanceof Playlist) {
			if (isPendingOrNullChildren(parentElement, tracksByPlaylist)) {
				Job job = createGetPlaylistChildrenJob((Playlist) parentElement);
				job.schedule();
				try {
					job.join();
					return getChildren(parentElement);
				} catch (InterruptedException e) {
				}
			} else {
				return (Object[]) tracksByPlaylist.get(parentElement);
			}
		}
		return new Object[0];
	}

	/**
	 * Checks whether the parent/child cache includes no children (or a
	 * temporary node) for a specific node.
	 * 
	 * @param parentElement
	 *            a node.
	 * @param parentChildMap
	 *            the parent/child cache map.
	 * @return <code>true</code> if the cache includes no children (or a
	 *         temporary node) for <i>parentElement</i>.
	 */
	private boolean isPendingOrNullChildren(Object parentElement,
			Map parentChildMap) {
		Object[] children = (Object[]) parentChildMap.get(parentElement);
		if (children == null) {
			return true;
		} else if (children.length == 1) {
			return (children[0] instanceof PendingNode);
		}
		return false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
	 */
	public Object getParent(Object element) {
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
	 */
	public Object[] getElements(Object inputElement) {
		return getChildren(inputElement);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
	 */
	public boolean hasChildren(Object element) {
		if (element instanceof Track) {
			return false;
		} else if (element instanceof Album) {
			return ((Album) element).getTrackCount() > 0;
		} else if (element instanceof Artist) {
			return ((Artist) element).getAlbumCount() > 0;
		} else if (element instanceof Playlist) {
			return ((Playlist) element).getFileCount() > 0;
		} else {
			return getChildren(element).length > 0;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer,
	 *      java.lang.Object, java.lang.Object)
	 */
	public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		this.viewer = (TreeViewer) viewer;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.IContentProvider#dispose()
	 */
	public void dispose() {
	}

}
